﻿// Copyright (c) 2014 Converter Systems LLC

using Opc.Ua;
using Opc.Ua.Client;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Threading;

namespace ConverterSystems.ServiceModel
{
    public abstract class BindingBase
    {
        protected readonly MonitoredItem _item;
        protected readonly PropertyInfo _property;
        protected readonly WeakReference<object> _targetRef;
        private static readonly SynchronizationContext _context = new SynchronizationContext();

        public BindingBase(Subscription source, MonitoredItem item, object target, PropertyInfo property)
        {
            if (source == null) throw new ArgumentNullException("source");
            if (item == null) throw new ArgumentNullException("item");
            if (target == null) throw new ArgumentNullException("target");
            if (property == null) throw new ArgumentNullException("property");
            _item = item;
            _targetRef = new WeakReference<object>(target);
            _property = property;
            if (source.FastDataChangeCallback == null)
            {
                source.FastDataChangeCallback = OnDataChange;
                source.FastEventCallback = OnEvent;
                source.DisableMonitoredItemCache = true;
                source.StateChanged += OnStateChanged;
            }
            if (source.Created)
            {
                Attach();
            }
        }

        protected virtual void Attach()
        {
        }

        protected virtual void Detach()
        {
        }

        protected virtual void SetDataValue(object target, DataValue value)
        {
        }

        protected virtual void SetEventFields(object target, EventFieldList fields)
        {
        }

        private static void CancelSubscription(Subscription subscription)
        {
            if (subscription == null)
                throw new ArgumentNullException("subscription");
            var session = subscription.Session;
            if (session != null)
            {
                session.RemoveSubscription(subscription);
                Trace.TraceInformation("Success cancelling subscription '{0}'", subscription.DisplayName);
            }
        }

        private void OnDataChange(Subscription subscription, DataChangeNotification notification, IList<string> stringTable)
        {
            _context.Post(o =>
            {
                var state = (Tuple<Subscription, DataChangeNotification, WeakReference<object>>)o;
                object target;
                if (state.Item3.TryGetTarget(out target))
                {
                    foreach (var itemNotification in state.Item2.MonitoredItems)
                    {
                        var item = state.Item1.FindItemByClientHandle(itemNotification.ClientHandle);
                        if (item != null)
                        {
                            var b = item.Handle as BindingBase;
                            if (b != null)
                            {
                                b.SetDataValue(target, itemNotification.Value);
                            }
                        }
                    }
                }
                else
                {
                    CancelSubscription(state.Item1);
                }
            }, Tuple.Create(subscription, notification, _targetRef));
        }

        private void OnEvent(Subscription subscription, EventNotificationList notification, IList<string> stringTable)
        {
            _context.Post(o =>
            {
                var state = (Tuple<Subscription, EventNotificationList, WeakReference<object>>)o;
                object target;
                if (state.Item3.TryGetTarget(out target))
                {
                    foreach (var itemNotification in state.Item2.Events)
                    {
                        var item = state.Item1.FindItemByClientHandle(itemNotification.ClientHandle);
                        if (item != null)
                        {
                            var b = item.Handle as BindingBase;
                            if (b != null)
                            {
                                b.SetEventFields(target, itemNotification);
                            }
                        }
                    }
                }
                else
                {
                    CancelSubscription(state.Item1);
                }
            }, Tuple.Create(subscription, notification, _targetRef));
        }

        private void OnStateChanged(Subscription subscription, SubscriptionStateChangedEventArgs e)
        {
            if ((e.Status & SubscriptionChangeMask.Created) == SubscriptionChangeMask.Created)
            {
                foreach (var item in subscription.MonitoredItems)
                {
                    var b = item.Handle as BindingBase;
                    if (b != null)
                    {
                        b.Attach();
                    }
                }
            }
            if ((e.Status & SubscriptionChangeMask.Deleted) == SubscriptionChangeMask.Deleted)
            {
                foreach (var item in subscription.MonitoredItems)
                {
                    var b = item.Handle as BindingBase;
                    if (b != null)
                    {
                        b.Detach();
                    }
                }
            }
        }
    }

    /// <summary>
    /// Creates a two-way binding between an Opc.Ua.Client.MonitoredItem and a property of type DataValue.
    /// </summary>
    public class DataValueBinding : BindingBase
    {
        private bool _isDataChanging;

        public DataValueBinding(Subscription source, MonitoredItem item, object target, PropertyInfo property)
            : base(source, item, target, property)
        {
        }

        protected override void Attach()
        {
            object target;
            if (_targetRef.TryGetTarget(out target))
            {
                var inpc = target as INotifyPropertyChanged;
                if (inpc != null)
                {
                    PropertyChangedEventManager.AddHandler(inpc, OnPropertyChanged, _property.Name);
                }
            }
        }

        protected override void Detach()
        {
            object target;
            if (_targetRef.TryGetTarget(out target))
            {
                var inpc = target as INotifyPropertyChanged;
                if (inpc != null)
                {
                    PropertyChangedEventManager.RemoveHandler(inpc, OnPropertyChanged, _property.Name);
                }
            }
        }

        protected override void SetDataValue(object target, DataValue value)
        {
            _isDataChanging = true;
            try
            {
                _property.SetValue(target, value);
            }
            finally
            {
                _isDataChanging = false;
            }
        }

        private async void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (_isDataChanging)
            {
                return;
            }
            var value = _property.GetValue(sender) as DataValue;
            if (value == null)
            {
                return;
            }
            var subscription = _item.Subscription;
            if (subscription != null)
            {
                var session = subscription.Session as UaSession;
                if (session != null && session.Connected)
                {
                    try
                    {
                        var writeRequest = new WriteRequest { NodesToWrite = { new WriteValue { NodeId = _item.ResolvedNodeId, AttributeId = _item.AttributeId, Value = value } } };
                        var writeResponse = await session.WriteAsync(writeRequest).ConfigureAwait(false);
                        for (int i = 0; i < writeResponse.Results.Count; i++)
                        {
                            if (StatusCode.IsNotGood(writeResponse.Results[i]))
                            {
                                Trace.TraceError("Error writing value for NodeId {0} : {1}", writeRequest.NodesToWrite[i].NodeId, writeResponse.Results[i]);
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        Trace.TraceError("Error writing value for NodeId {0} : {1}", _item.ResolvedNodeId, ex.Message);
                    }
                }
                else
                {
                    Trace.TraceError("Error writing value for NodeId {0} : {1}", _item.ResolvedNodeId, "Session is null or not connected");
                }
            }
            else
            {
                Trace.TraceError("Error writing value for NodeId {0} : {1}", _item.ResolvedNodeId, "Subscription is null");
                //var inpc = sender as INotifyPropertyChanged;
                //if (inpc != null)
                //{
                //    PropertyChangedEventManager.RemoveHandler(inpc, OnPropertyChanged, _property.Name);
                //}
            }
        }
    }

    /// <summary>
    /// Creates a one-way binding between an Opc.Ua.Client.MonitoredItem and a property of type Queue&gtDataValue&lt.
    /// </summary>
    public class DataValueQueueBinding : BindingBase
    {
        private int _size;

        public DataValueQueueBinding(Subscription source, MonitoredItem item, object target, PropertyInfo property)
            : base(source, item, target, property)
        {
            _size = Math.Max(item.CacheQueueSize, 1);
        }

        protected override void SetDataValue(object target, DataValue value)
        {
            var q = _property.GetValue(target) as Queue<DataValue>;
            if (q != null)
            {
                while (q.Count >= _size) q.Dequeue();
                q.Enqueue(value);
            }
        }
    }

    /// <summary>
    /// Creates a one-way binding between an Opc.Ua.Client.MonitoredItem and a property of type EventFieldList.
    /// </summary>
    public class EventBinding : BindingBase
    {
        public EventBinding(Subscription source, MonitoredItem item, object target, PropertyInfo property)
            : base(source, item, target, property)
        {
        }

        protected override void SetEventFields(object target, EventFieldList fields)
        {
            _property.SetValue(target, fields);
        }
    }

    /// <summary>
    /// Creates a one-way binding between an Opc.Ua.Client.MonitoredItem and a property of type Queue&gtEventFieldList&lt.
    /// </summary>
    public class EventQueueBinding : BindingBase
    {
        private int _size;

        public EventQueueBinding(Subscription source, MonitoredItem item, object target, PropertyInfo property)
            : base(source, item, target, property)
        {
            _size = Math.Max(item.CacheQueueSize, 1);
        }

        protected override void SetEventFields(object target, EventFieldList fields)
        {
            var q = _property.GetValue(target) as Queue<EventFieldList>;
            if (q != null)
            {
                while (q.Count >= _size) q.Dequeue();
                q.Enqueue(fields);
            }
        }
    }

    /// <summary>
    /// Creates a two-way binding between an Opc.Ua.Client.MonitoredItem and a property.
    /// </summary>
    public class ValueBinding : BindingBase
    {
        private bool _isDataChanging;

        public ValueBinding(Subscription source, MonitoredItem item, object target, PropertyInfo property)
            : base(source, item, target, property)
        {
        }

        protected override void Attach()
        {
            object target;
            if (_targetRef.TryGetTarget(out target))
            {
                var inpc = target as INotifyPropertyChanged;
                if (inpc != null)
                {
                    PropertyChangedEventManager.AddHandler(inpc, OnPropertyChanged, _property.Name);
                }
            }
        }

        protected override void Detach()
        {
            object target;
            if (_targetRef.TryGetTarget(out target))
            {
                var inpc = target as INotifyPropertyChanged;
                if (inpc != null)
                {
                    PropertyChangedEventManager.RemoveHandler(inpc, OnPropertyChanged, _property.Name);
                }
            }
        }

        protected override void SetDataValue(object target, DataValue value)
        {
            _isDataChanging = true;
            try
            {
                if (StatusCode.IsGood(value.StatusCode))
                {
                    try
                    {
                        _property.SetValue(target, value.GetValue(_property.PropertyType));
                    }
                    catch (ServiceResultException)
                    {
                        Trace.TraceError("Error setting value of property '{0}', received type '{1}', expected type '{2}'.", _property.Name, value.WrappedValue.TypeInfo, _property.PropertyType);
                    }
                }
                else
                {
                    Trace.TraceError("Error setting value of property '{0}', StatusCode '{1}'.", _property.Name, value.StatusCode);
                }
            }
            finally
            {
                _isDataChanging = false;
            }
        }

        private async void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (_isDataChanging)
            {
                return;
            }
            var value = new DataValue(new Variant(_property.GetValue(sender)));
            if (value != null)
            {
                var subscription = _item.Subscription;
                if (subscription != null)
                {
                    var session = subscription.Session as UaSession;
                    if (session != null && session.Connected)
                    {
                        try
                        {
                            var writeRequest = new WriteRequest { NodesToWrite = { new WriteValue { NodeId = _item.ResolvedNodeId, AttributeId = _item.AttributeId, Value = value } } };
                            var writeResponse = await session.WriteAsync(writeRequest).ConfigureAwait(false);
                            for (int i = 0; i < writeResponse.Results.Count; i++)
                            {
                                if (StatusCode.IsNotGood(writeResponse.Results[i]))
                                {
                                    Trace.TraceError("Error writing value for NodeId {0} : {1}", writeRequest.NodesToWrite[i].NodeId, writeResponse.Results[i]);
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            Trace.TraceError("Error writing value for NodeId {0} : {1}", _item.ResolvedNodeId, ex.Message);
                        }
                    }
                    else
                    {
                        Trace.TraceError("Error writing value for NodeId {0} : {1}", _item.ResolvedNodeId, "Session is null or not connected");
                    }
                }
                else
                {
                    Trace.TraceError("Error writing value for NodeId {0} : {1}", _item.ResolvedNodeId, "Subscription is null");
                }
            }
        }
    }
}